// Copyright (c) 2011 Blue Onion Software, All rights reserved
using System;
using System.Collections.Generic;
using System.Diagnostics;
using System.Diagnostics.CodeAnalysis;
using System.Globalization;
using System.IO;
using System.Threading;
using System.Web.Configuration;
using System.Web.Hosting;

namespace BlueOnionSoftware.Bloget
{
    internal enum LogCategory
    {
        None,
        Error = 100,
        Authorization = 200,
        Administration = 201,
        EditUserSetting = 202,
        RpcRequest = 300,
        PingBack = 301,
        RpcRequestAccepted = 302,
        RpcRequestRejected = 303,
        Posting = 400,
        Ping = 401
    }

    internal static class Log
    {
        private static readonly ThreadSafeQueue _queue;
        private static readonly AutoResetEvent _spoolEvent;
        private static readonly string _virtualLogFolder;

        static Log()
        {
            _queue = new ThreadSafeQueue();
            _spoolEvent = new AutoResetEvent(false);
            _virtualLogFolder = VirtualLogFolderSetting();
            ThreadPool.RegisterWaitForSingleObject(_spoolEvent, SpoolerProc, null, -1, false);
        }

        internal static string VirtualLogFolder
        {
            get { return _virtualLogFolder; }
        }

        internal static string LogFilePattern
        {
            get { return "????-??-??.bloget.log"; }
        }

        internal static void NewPost(Bloget bloget, Guid postId, string title)
        {
            Add(LogCategory.Posting, bloget, "New post - {0} ({1})", title, postId);
        }

        internal static void EditPost(Bloget bloget, Guid postId, string title)
        {
            Add(LogCategory.Posting, bloget, "Edit post - {0} ({1})", title, postId);
        }

        internal static void RemovePost(Bloget bloget, Guid postId)
        {
            Add(LogCategory.Posting, bloget, "Remove post - {0}", postId);
        }

        internal static void GetPost(Bloget bloget, Guid postId)
        {
            Add(LogCategory.Posting, bloget, "Get post - {0}", postId);
        }

        internal static void NewComment(Bloget bloget, string postTitle, Guid commentId)
        {
            Add(LogCategory.Posting, bloget, "New comment - {0}({1})", postTitle, commentId);
        }

        internal static void RejectComment(Bloget bloget, string postTitle, string comment)
        {
            Add(LogCategory.Posting, bloget, "Reject comment - {0}({1})", postTitle,
                comment.Substring(0, Math.Min(256, comment.Length)));
        }

        internal static void RemoveComment(Bloget bloget, string postTitle, Guid commentId)
        {
            Add(LogCategory.Posting, bloget, "Remove comment - {0}({1})", postTitle, commentId);
        }

        internal static void EmailSent(Bloget bloget, string from, string to, string subject)
        {
            Add(LogCategory.Posting, bloget, "Email sent - From:{0} To:{1} Subject:{2}", from, to, subject);
        }

        internal static void Login(Bloget bloget)
        {
            Add(LogCategory.Authorization, bloget, "Login");
        }

        internal static void Logout(Bloget bloget)
        {
            Add(LogCategory.Authorization, bloget, "Logout");
        }

        internal static void LoginFail(Bloget bloget)
        {
            Add(LogCategory.Error, bloget, "Login attempt failed");
        }

        internal static void LicenseAgreementAccepted(Bloget bloget)
        {
            Add(LogCategory.Authorization, bloget, "License agreement accepted");
        }

        internal static void PingFailed(Bloget bloget, string link, string error)
        {
            Add(LogCategory.Error, bloget, "Ping failed ({1}) {0}", link, error);
        }

        internal static void PingSucceeded(Bloget bloget, string link, string message)
        {
            Add(LogCategory.Ping, bloget, "Ping succeeded ({1}) {0}", link, message);
        }

        internal static void PingBackReceived(Bloget bloget, string source, string target)
        {
            Add(LogCategory.PingBack, bloget, "Pingback received {0}, {1}", source, target);
        }

        internal static void PingBackFailed(Bloget bloget, string source, string target, string reason)
        {
            Add(LogCategory.RpcRequestRejected, bloget, "Pingback rejected ({2}) {0}, {1}", source, target, reason);
        }

        internal static void RemovePingBack(Bloget bloget, Guid postId, Guid pingBackId)
        {
            Add(LogCategory.PingBack, bloget, "Remove pingback - {0}({1})", postId, pingBackId);
        }

        internal static void RpcRequest(Bloget bloget, string methodCall)
        {
            Add(LogCategory.RpcRequest, bloget, "Received RPC Request: {0}", methodCall);
        }

        internal static void RpcRequestAccepted(Bloget bloget, string methodCall)
        {
            Add(LogCategory.RpcRequestAccepted, bloget, "RPC Request: {0} accepted", methodCall);
        }

        internal static void RpcRequestRejected(Bloget bloget, string methodCall, string reason)
        {
            Add(LogCategory.RpcRequestRejected, bloget, "RPC Request: {0} rejected : {1}", methodCall, reason);
        }

        internal static void Administration(Bloget bloget, string setting, string oldValue, string newValue)
        {
            Add(LogCategory.Administration, bloget, "Admin: {0} = {2} ({1})", setting, oldValue, newValue);
        }

        internal static void EditUser(Bloget bloget, string user, string action)
        {
            Add(LogCategory.EditUserSetting, bloget, "EditUser: {0}, Action: {2}", user, action);
        }

        internal static void Exception(Bloget bloget, string where, Exception ex)
        {
            Add(LogCategory.Error, bloget, "Exception in {0} - {1}", where, ex.Message);
        }

        internal static void ExceptionStackTrace(Bloget bloget, string where, Exception ex)
        {
            Add(LogCategory.Error, bloget, "Exception in {0} - {1}", where, ex.ToString());
        }

        [SuppressMessage("Microsoft.Design", "CA1031:DoNotCatchGeneralExceptionTypes")]
        private static void Add(LogCategory logCategory, Bloget bloget, string format, params object[] args)
        {
            try
            {
                // Copy the strings so a reference to the control is not held
                var id = string.Copy(bloget.ID ?? "no-id");
                string referrer;

                try
                {
                    // Accessing Request cross thread (like in pingbacks) can raise exceptions
                    referrer = string.Copy((bloget.Page.Request.UrlReferrer != null)
                        ? bloget.Page.Request.UrlReferrer.AbsoluteUri
                        : bloget.Page.Request.ServerVariables["REMOTE_ADDR"]);
                }

                catch (Exception)
                {
                    referrer = "no-referrer";
                }

                var message = string.Format(CultureInfo.InvariantCulture, format, args);
                var logDataItem = new LogDataItem(logCategory, DateTime.UtcNow, id, referrer, message);
                _queue.Enqueue(logDataItem.ToString());
                _spoolEvent.Set();
            }

            catch (Exception ex)
            {
                Debug.Assert(false, ex.Message, ex.ToString());
            }
        }

        [SuppressMessage("Microsoft.Design", "CA1031:DoNotCatchGeneralExceptionTypes")]
        private static void SpoolerProc(object state, bool timeout)
        {
            try
            {
                var virtualLogPath = string.Format(CultureInfo.InvariantCulture,
                                                   "{0}{1}.bloget.log", _virtualLogFolder,
                                                   DateTime.UtcNow.ToString("yyyy-MM-dd",
                                                                            CultureInfo.InvariantCulture));

                var physicalLogPath = HostingEnvironment.MapPath(virtualLogPath);

                using (var logStream = FileUtilities.OpenAppend(physicalLogPath))
                {
                    using (var logWriter = new StreamWriter(logStream))
                    {
                        string message;

                        while ((message = _queue.Dequeue()) != null)
                        {
                            logWriter.WriteLine(message);
                        }
                    }
                }
            }

            catch (Exception ex)
            {
                Debug.Assert(false, ex.Message, ex.ToString());
            }
        }

        [SuppressMessage("Microsoft.Design", "CA1031:DoNotCatchGeneralExceptionTypes")]
        private static string VirtualLogFolderSetting()
        {
            var result = "~/App_Data/";

            try
            {
                // Assumption: web.config is in the root path of the web application

                var folder = WebConfigurationManager.AppSettings["BlogetLogFolder"];

                if (string.IsNullOrEmpty(folder) == false)
                {
                    folder = folder.Trim();

                    if (folder.StartsWith("~", StringComparison.Ordinal))
                    {
                        if (folder.EndsWith("/", StringComparison.Ordinal) == false)
                        {
                            folder += "/";
                        }

                        result = folder;
                    }
                }
            }

            catch (Exception ex)
            {
                Debug.Assert(false, ex.Message, ex.ToString());
            }

            return result;
        }
    }

    internal class ThreadSafeQueue
    {
        private readonly object _synchronize = new object();
        private readonly Queue<string> _queue = new Queue<string>();

        internal void Enqueue(string message)
        {
            lock (_synchronize)
            {
                if (_queue.Count >= 1000)
                {
                    _queue.Dequeue();
                }

                _queue.Enqueue(message);
            }
        }

        internal string Dequeue()
        {
            lock (_synchronize)
            {
                return (_queue.Count > 0) ? _queue.Dequeue() : null;
            }
        }
    }

    internal class LogDataItem
    {
        private readonly LogCategory _logCategory;
        private readonly DateTime _time;
        private string _message;
        private readonly string _referrer;
        private readonly string _controlId;

        internal LogDataItem(LogCategory logCategory, DateTime time, string controlId, string referrer, string message)
        {
            _logCategory = logCategory;
            _time = time;
            _referrer = referrer ?? string.Empty;
            _message = message ?? string.Empty;
            _controlId = controlId ?? string.Empty;
        }

        public override string ToString()
        {
            _message = _message.Replace('\r', ' ');
            _message = _message.Replace('\n', ' ');

            return string.Format(CultureInfo.InvariantCulture,
                                 "{0} {1:s} {2,-15} {3,-15} {4}", Enum.Format(typeof (LogCategory), _logCategory, "d"),
                                 _time, _controlId, _referrer, _message);
        }
    }
}